﻿/*
Copyright © 2010 François Karman

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

See <license.txt> for the complete license of the software.
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace Faz.Dependencies
{
    /// <summary>
    /// Specialized class that loads the assemblies of a directory
    /// to build a collection of <see cref="AssemblyOverview"/>
    /// objects.
    /// </summary>
    public class AssemblyOverviewLoader
    {
        /// <summary>
        /// The valid extensions of the assemblies.
        /// </summary>
        private static readonly string[] Extensions = new string[] { "*.dll", "*.exe" };

        /// <summary>
        /// The reference directory where the assemblies are stored.
        /// </summary>
        private DirectoryInfo directory;

        /// <summary>
        /// The result collection of <see cref="AssemblyOverview"/> instances.
        /// </summary>
        private ICollection<AssemblyOverview> result;

        /// <summary>
        /// The loaded assemblies.
        /// </summary>
        private IDictionary<string, Assembly> assemblies;

        /// <summary>
        /// The generated overviews.
        /// </summary>
        private IDictionary<string, AssemblyOverview> overviews;

        /// <summary>
        /// The inverted graph.
        /// </summary>
        private IDictionary<string, ICollection<string>> inverses;

        /// <summary>
        /// Initializes a new instance of the AssemblyOverviewLoader class.
        /// </summary>
        /// <param name="directory">
        /// The reference directory where the assemblies are stored.
        /// </param>
        /// <param name="result">
        /// The result collection of <see cref="AssemblyOverview"/> instances.
        /// </param>
        public AssemblyOverviewLoader(DirectoryInfo directory, ICollection<AssemblyOverview> result)
        {
            this.directory = directory;
            this.result = result;
            this.assemblies = new Dictionary<string, Assembly>();
            this.overviews = new Dictionary<string, AssemblyOverview>();
            this.inverses = new Dictionary<string, ICollection<string>>();
        }

        /// <summary>
        /// Starts the extraction process.
        /// </summary>
        public void Extract()
        {
            // Clean all the collections
            this.result.Clear();
            this.assemblies.Clear();
            this.overviews.Clear();
            this.inverses.Clear();

            // Load assemblies from the directory
            ICollection<Assembly> loaded = new LinkedList<Assembly>();
            foreach (string extension in Extensions)
            {
                foreach (FileInfo file in this.directory.GetFiles(extension, SearchOption.TopDirectoryOnly))
                {
                    // TODO: Manage exceptions
                    Assembly assembly = Assembly.ReflectionOnlyLoadFrom(file.FullName);
                    loaded.Add(assembly);
                    this.assemblies.Add(assembly.GetName().FullName, assembly);
                }
            }

            // Build the result without level
            foreach (Assembly assembly in loaded)
            {
                this.CreateOverview(assembly.GetName().FullName, assembly);
            }

            // Define the level of each assembly
            foreach (AssemblyOverview overview in this.overviews.Values)
            {
                this.SetLevel(overview, new List<string>());
            }

            // Define the category of each assembly
            foreach (Assembly assembly in loaded)
            {
                AssemblyOverview overview = this.overviews[assembly.GetName().FullName];
                overview.Category = Category.InitialAssembly;
                foreach (AssemblyOverview dependency in overview.Dependencies)
                {
                    if (dependency.Category != Category.InitialAssembly)
                    {
                        dependency.Category = Category.IndirectAssembly;
                    }
                }
            }

            // Update the initial collection.
            // In fact, this should be the last step to be sure that the elements
            // are fully initialized when displayed.
            foreach (AssemblyOverview overview in this.overviews.Values)
            {
                this.result.Add(overview);
            }
        }

        /// <summary>
        /// Updates the level of an overview and of all its parent.
        /// </summary>
        /// <param name="overview">The overview to update.</param>
        /// <param name="walked">The current walked element - used to avoid cycle.</param>
        private void SetLevel(AssemblyOverview overview, ICollection<string> walked)
        {
            walked.Add(overview.FullName);

            if (overview.Level != -1)
            {
                // Already done
                return;
            }

            string fullName = overview.FullName;
            if (!this.inverses.ContainsKey(fullName) || this.inverses[fullName].Count == 0)
            {
                // No parent
                overview.Level = 0;
            }
            else
            {
                // Some parents
                int max = -1;
                foreach (string parentName in this.inverses[fullName])
                {
                    if (!walked.Contains(parentName))
                    {
                        this.SetLevel(this.overviews[parentName], walked);
                    }

                    max = Math.Max(this.overviews[parentName].Level, max);
                }

                overview.Level = max + 1;
            }
        }

        /// <summary>
        /// Initializes the overviews collection by adding an assembly and all
        /// its dependencies.
        /// </summary>
        /// <param name="fullName">
        /// The full name of the assembly.
        /// </param>
        /// <param name="assembly">
        /// The assembly.
        /// </param>
        /// <remarks>
        /// The full name is separated from the assembly to avoid issue when
        /// automapping is used between the requirement of the library and
        /// the existing version of the user computer.
        /// </remarks>
        private void CreateOverview(string fullName, Assembly assembly)
        {
            if (this.overviews.ContainsKey(fullName))
            {
                // Already loaded - nothing to do
                return;
            }

            AssemblyOverview overview = new AssemblyOverview();
            overview.Name = assembly.GetName().Name;
            overview.FullName = fullName;

            this.overviews.Add(fullName, overview);

            foreach (AssemblyName name in assembly.GetReferencedAssemblies())
            {
                // Check if the dependency should be loaded
                if (!this.assemblies.ContainsKey(name.FullName))
                {
                    try
                    {
                        Assembly dependency = Assembly.ReflectionOnlyLoad(name.FullName);
                        this.assemblies.Add(name.FullName, dependency);
                    }
                    catch (Exception exception)
                    {
                        Trace.Default.TraceInformation(
                            "Can't load the assembly <{0}> due to an error <{1}>",
                            name.Name,
                            exception.Message);
                        continue;
                    }
                }

                if (!this.overviews.ContainsKey(name.FullName))
                {
                    this.CreateOverview(name.FullName, this.assemblies[name.FullName]);
                }

                overview.Dependencies.Add(this.overviews[name.FullName]);

                // Update the inverted graph
                if (!this.inverses.ContainsKey(name.FullName))
                {
                    this.inverses.Add(name.FullName, new LinkedList<string>());
                }

                this.inverses[name.FullName].Add(fullName);
            }
        }
    }
}
